package main

import (
	"context"
	"elastic_exporter/collector"
	"fmt"
	"net/http"
	"os"
	"path"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/elastic/go-elasticsearch/v7"

	"github.com/go-kit/log"
	"github.com/go-kit/log/level"

	"github.com/pkg/errors"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"github.com/prometheus/common/promlog"
	"github.com/prometheus/common/promlog/flag"
	"github.com/prometheus/common/version"
	"github.com/prometheus/exporter-toolkit/web"
	webflag "github.com/prometheus/exporter-toolkit/web/kingpinflag"

	"gopkg.in/alecthomas/kingpin.v2"
	"gopkg.in/ini.v1"
)

const program = "elastic_exporter"

var (
	webConfig = webflag.AddFlags(kingpin.CommandLine)

	// web服务，默认使用9104端口
	listenAddress = kingpin.Flag(
		"web.listen",
		"Address to listen on for web interface and telemetry.",
	).Default(":9104").String()

	// 指定配置文件
	configCnf = kingpin.Flag(
		"config",
		"Path to golden.cnf file to read GoldenDB credentials from.",
	).Default(path.Join(os.Getenv("HOME"), "golden.cnf")).String()

	// metrics
	metricPath = kingpin.Flag(
		"web.telemetry-path",
		"Path under which to expose metrics.",
	).Default("/metrics").String()

	// 超时偏移
	timeoutOffset = kingpin.Flag(
		"timeout-offset",
		"Offset to subtract from timeout in seconds.",
	).Default("0.25").Float64()

	// 连接信息字符串
	elasticUrl        string
	elasticAuthUser   string
	elasticAuthPasswd string

	elasticConfig elasticsearch.Config
)

// scrapers lists all possible collection methods and if they should be enabled by default.
var scrapers = map[collector.Scraper]bool{}

func init() {
	prometheus.MustRegister(version.NewCollector(program))
}

func main() {
	// Generate ON/OFF flags for all scrapers.
	scraperFlags := map[collector.Scraper]*bool{}
	for scraper, enabledByDefault := range scrapers {
		defaultOn := "false"
		if enabledByDefault {
			defaultOn = "true"
		}

		f := kingpin.Flag(
			"collect."+scraper.Name(),
			scraper.Help(),
		).Default(defaultOn).Bool()

		scraperFlags[scraper] = f
	}

	// Parse flags.
	promlogConfig := &promlog.Config{}
	flag.AddFlags(kingpin.CommandLine, promlogConfig)
	kingpin.Version(version.Print(program))
	kingpin.HelpFlag.Short('h')
	kingpin.Parse()
	logger := promlog.New(promlogConfig)

	level.Info(logger).Log("msg", "Starting "+program, "version", version.Info())
	level.Info(logger).Log("msg", "Build context", version.BuildContext())

	// 读取环境变量，不存在时读取配置文件./elastic.cnf
	// ELASTIC_URL
	// ELASTIC_AUTH_USER
	// ELASTIC_AUTH_PASSWD
	elasticAuthUser = os.Getenv("ELASTIC_AUTH_USER")
	elasticAuthPasswd = os.Getenv("ELASTIC_AUTH_PASSWD")

	elasticUrl = os.Getenv("ELASTIC_URL")
	if len(elasticUrl) == 0 {
		var err error
		if err = parseCnf(*configCnf); err != nil {
			level.Info(logger).Log("msg", "Error parsing elastic.cnf", "file", *configCnf, "err", err)
			os.Exit(1)
		}
	}

	// Register only scrapers enabled by flag.
	enabledScrapers := []collector.Scraper{}
	for scraper, enabled := range scraperFlags {
		if *enabled {
			level.Info(logger).Log("msg", "Scraper enabled", "scraper", scraper.Name())
			enabledScrapers = append(enabledScrapers, scraper)
		}
	}

	// path '/metric'
	handlerFunc := newHandler(collector.NewMetrics(), enabledScrapers, logger)
	http.Handle(*metricPath, promhttp.InstrumentMetricHandler(prometheus.DefaultRegisterer, handlerFunc))

	// landingPage contains the HTML served at '/'.
	var landingPage = []byte(`
		<html>
		<head><title>GoldenDB exporter</title></head>
		<body>
			<h1>GoldenDB exporter</h1>
			<p><a href='` + *metricPath + `'>Metrics</a></p>
		</body>
		</html>
	`)
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write(landingPage)
	})

	// 启动服务，监听端口
	level.Info(logger).Log("msg", "Listening on address", "address", *listenAddress)
	srv := &http.Server{Addr: *listenAddress}
	if err := web.ListenAndServe(srv, *webConfig, logger); err != nil {
		level.Error(logger).Log("msg", "Error starting HTTP server", "err", err)
		os.Exit(1)
	}
}

// 解析配置文件
func parseCnf(config interface{}) error {
	opts := ini.LoadOptions{
		// Elastic ini file can have boolean keys.
		AllowBooleanKeys: true,
	}
	cfg, err := ini.LoadSources(opts, config)
	if err != nil {
		return fmt.Errorf("failed reading ini file: %s", err)
	}

	// 连接地址
	address := cfg.Section("client").Key("address").MustString("localhost:9200")
	reg := regexp.MustCompile("\\s+")
	elasticUrl = reg.ReplaceAllString(address, "")
	if len(elasticUrl) == 0 {
		return errors.New("elasticsearch node address cannot be empty.")
	}

	// 认证信息
	user := cfg.Section("client").Key("user").String()
	if user != "" {
		elasticAuthUser = user
	}
	passwd := cfg.Section("client").Key("password").String()
	if passwd != "" {
		elasticAuthPasswd = passwd
	}

	return nil
}

func newHandler(metrics collector.Metrics, scrapers []collector.Scraper, logger log.Logger) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		filteredScrapers := scrapers
		params := r.URL.Query()["collect[]"]
		// Use request context for cancellation when connection gets closed.
		ctx := r.Context()
		// If a timeout is configured via the Prometheus header, add it to the context.
		if v := r.Header.Get("X-Prometheus-Scrape-Timeout-Seconds"); v != "" {
			timeoutSeconds, err := strconv.ParseFloat(v, 64)
			if err != nil {
				level.Error(logger).Log("msg", "Failed to parse timeout from Prometheus header", "err", err)
			} else {
				if *timeoutOffset >= timeoutSeconds {
					// Ignore timeout offset if it doesn't leave time to scrape.
					level.Error(logger).Log("msg", "Timeout offset should be lower than prometheus scrape timeout", "offset", *timeoutOffset, "prometheus_scrape_timeout", timeoutSeconds)
				} else {
					// Subtract timeout offset from timeout.
					timeoutSeconds -= *timeoutOffset
				}
				// Create new timeout context with request context as parent.
				var cancel context.CancelFunc
				ctx, cancel = context.WithTimeout(ctx, time.Duration(timeoutSeconds*float64(time.Second)))
				defer cancel()
				// Overwrite request with timeout context.
				r = r.WithContext(ctx)
			}
		}
		level.Debug(logger).Log("msg", "collect[] params", "params", strings.Join(params, ","))

		// Check if we have some "collect[]" query parameters.
		if len(params) > 0 {
			filters := make(map[string]bool)
			for _, param := range params {
				filters[param] = true
			}

			filteredScrapers = nil
			for _, scraper := range scrapers {
				if filters[scraper.Name()] {
					filteredScrapers = append(filteredScrapers, scraper)
				}
			}
		}

		registry := prometheus.NewRegistry()
		registry.MustRegister(collector.New(ctx, elasticUrl, elasticAuthUser, elasticAuthPasswd, metrics, filteredScrapers, logger))

		gatherers := prometheus.Gatherers{
			prometheus.DefaultGatherer,
			registry,
		}
		// Delegate http serving to Prometheus client library, which will call collector.Collect.
		h := promhttp.HandlerFor(gatherers, promhttp.HandlerOpts{})
		h.ServeHTTP(w, r)
	}
}
